package opencm;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;

import org.apache.commons.lang3.Validate;
import org.joda.time.Duration;
import org.joda.time.LocalTime;

public final class Medico implements Serializable {

	private final UUID id;
	private Perfil perfil;
	private Especialidade especialidade;
	private Duration duracaoPrimeiraConsulta = null;
	private Duration duracaoConsultaRetorno = null;
	private Duration duracaoCirugia = null;
	private LocalTime horarioSaida = null;
	private Set<Horario> horarios = new HashSet<Horario>();

	private static final long serialVersionUID = 1L;

	/**
	 * Contrutor unico de Medico que so recebe obrigatoriamente o perfil e uma especialidade medica.
	 * @param perfil Perfil do medico.
	 * @param especialidade Enum especialidade do medico.
	 */
	public Medico(Perfil perfil, Especialidade especialidade) {
		setPerfil(perfil);
		setEspecialidade(especialidade);
		id = UUID.randomUUID();
	}

	@Override
	public boolean equals(Object obj) {
		if (obj == null)
			return false;
		if (obj == this)
			return true;
		if (obj instanceof Medico) {
			Medico m = (Medico) obj;
			return m.id().equals(id());
		}
		return false;
	}
	
	@Override
	public int hashCode() {
		return id().hashCode();
	}
	
	@Override
	public String toString() {
		return getPerfil().getNome();
	}

	/**
	 * Retora o id do medico.
	 * @return UUID
	 */
	public UUID id() {
		return this.id;
	}

	/**
	 * Calcula e retorna a duracao da consulta marcada.
	 * @param consulta Consulta marcada.
	 * @return Duracao da consulta.
	 */
	public Duration calculaDuracaoConsulta(Consulta consulta) {
		if (consulta.getTipo().equals(Consulta.Tipo.CIRUGIA)) {
			return (getDuracaoCirugia() != null) ? getDuracaoCirugia() : getEspecialidade()
					.getDuracaoCirugia();
		} else if (consulta.isRetorno()) {
			return (getDuracaoConsultaRetorno() != null) ? getDuracaoConsultaRetorno() : getEspecialidade()
					.getDuracaoConsultaRetorno();
		} else {
			return (getDuracaoPrimeiraConsulta() != null) ? getDuracaoPrimeiraConsulta() : getEspecialidade()
					.getDuracaoPrimeiraConsulta();
		}
	}

	/**
	 * Muda o perfil do medico.
	 * @param perfil Novo perfil.
	 */
	public void setPerfil(Perfil perfil) {
		Validate.notNull(perfil, "Perfil não pode ser nulo.");
		this.perfil = perfil;
	}

	/**
	 * Muda a especialidade do medico.
	 * @param especialidade Nova especialidade.
	 */
	public void setEspecialidade(Especialidade especialidade) {
		Validate.notNull(especialidade, "Especialidade não pode ser nula.");
		this.especialidade = especialidade;
	}

	/**
	 * Muda a duracao media da primeira consulta.
	 * @param duracao Nova duracao.
	 */
	public void setDuracaoPrimeiraConsulta(Duration duracao) {
		this.duracaoPrimeiraConsulta = duracao;
	}

	/**
	 * Muda a duracao media da consulta de retorno.
	 * @param duracao Nova duracao.
	 */
	public void setDuracaoConsultaRetorno(Duration duracao) {
		this.duracaoConsultaRetorno = duracao;
	}

	/**
	 * Muda a duracao media de uma cirurgia do medico. 
	 * @param duracao Nova duracao.
	 */
	public void setDuracaoCirugia(Duration duracao) {
		this.duracaoCirugia = duracao;
	}

	/**
	 * Muda horario de saida desejado pelo medico.
	 * @param horarioSaida Novo horario.
	 */
	public void setHorarioSaida(LocalTime horarioSaida) {
		this.horarioSaida = horarioSaida;
	}

	/**
	 * Retorna o perfil do medico.
	 * @return Perfil
	 */
	public Perfil getPerfil() {
		return this.perfil;
	}

	/**
	 * Retorna a especialidade do medico.
	 * @return Enum Especialidade
	 */
	public Especialidade getEspecialidade() {
		return this.especialidade;
	}

	/**
	 * Retorna a duracao media da primeira consulta do medico.
	 * @return Duration
	 */
	public Duration getDuracaoPrimeiraConsulta() {
		return this.duracaoPrimeiraConsulta;
	}

	/**
	 * Retorna a duracao media da consulta de retorno do medico.
	 * @return Duration
	 */
	public Duration getDuracaoConsultaRetorno() {
		return this.duracaoConsultaRetorno;
	}

	/**
	 * Retorna a duracao media de uma cirurgia do medico.
	 * @return Duration
	 */
	public Duration getDuracaoCirugia() {
		return this.duracaoCirugia;
	}

	/**
	 * Retorna o horario desejado de saida do medico.
	 * @return LocalTime
	 */
	public LocalTime getHorarioSaida() {
		return this.horarioSaida;
	}

	/**
	 * Adiciona um horario a lista de horarios do medico.
	 * @param horario Novo horario.
	 */
	public void adicionarHorario(Horario horario) {
		Validate.notNull(horario, "O Horário não pode ser nulo.");
		Validate.isTrue(!hasConflitoHorario(horario), "O horário não pode causar conflitos.");
		this.horarios.add(horario);
	}

	/**
	 * Remove um horario da lista de horarios do medico.
	 * @param horario Horario a ser removido.
	 * @return Boolean
	 */
	public boolean removerHorario(Horario horario) {
		return this.horarios.remove(horario);
	}

	/**
	 * Retorna a lista de horarios do medico.
	 * @return Lista de horarios.
	 */
	public List<Horario> getHorarios() {
		return new ArrayList<Horario>(this.horarios);
	}

	/**
	 * Retorna uma lista de horarios do tipo especifico de consulta.
	 * @param tipo Tipo de consulta.
	 * @return Lista de horarios.
	 */
	public List<Horario> getHorarios(Consulta.Tipo tipo) {
		List<Horario> result = new ArrayList<Horario>();

		for (Horario horario : getHorarios()) {
			if (horario.getTipo().equals(tipo)) {
				result.add(horario);
			}
		}

		return result;
	}

	/**
	 * Retorna uma lista de horarios do tipo especifico de consulta e de um certo dia da semana.
	 * @param tipo Tipo de consulta.
	 * @param diaSemana Dia da semana.
	 * @return Lista de horarios.
	 */
	public List<Horario> getHorarios(Consulta.Tipo tipo, DiaSemana diaSemana) {
		List<Horario> result = new ArrayList<Horario>();

		for (Horario horario : getHorarios()) {
			if (horario.getDiaSemana().equals(diaSemana) && horario.getTipo().equals(tipo)) {
				result.add(horario);
			}
		}

		return result;
	}

	/**
	 * Testa se o novo horario tem choque de horario com os horarios da lista do medico.
	 * @param novoHorario Horario a ser testado.
	 * @return Boolean
	 */
	private boolean hasConflitoHorario(Horario novoHorario) {
		for (Horario horario : getHorarios(novoHorario.getTipo(),
				novoHorario.getDiaSemana())) {
			if (horario.toIntervalToday().overlaps(novoHorario.toIntervalToday())) {
				return true;
			}
		}

		return false;
	}

}
